/**
 * @date 2016年3月6日 下午8:42:36
 * @version V1.0
 */
package edu.cn.nlsde.tmfst.lda;

import edu.cn.nlsde.Utils.IOUtil;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author ffftzh
 * @ClassName: LDA
 * @Description: LDA的实现
 * @date 2016年3月6日 下午8:42:36
 */
public class LDA_one_word implements Runnable {
    private final String data_path;
    private final double alpha;
    private final double beta;
    private final int iter_num;
    private final int topic_num;
    private final String outputPath;

    private HashMap<String, Integer> wordMap = new HashMap<>();
    private HashMap<Integer, String> id2Word = new HashMap<>();
    private HashMap<Integer, Integer> wordId2TopicId = new HashMap<>();
    private HashMap<Integer, Integer> wordNum = new HashMap<>();
    private HashMap<Integer, ArrayList<Integer>> wordInDocIndex = new HashMap<>();

    private int[][] wordId_of_corpus = null;

    private int[][] word_num_in_topic_word = null;
    private int[] word_num_in_topic = null;
    private int[][] word_num_in_doc_topic = null;
//    private int[] word_num_in_doc = null;


    public LDA_one_word(String data_path, int topic_num, int iter_num, double alpha, double beta, int threadId) {

        this.alpha = alpha;
        this.beta = beta;
        this.iter_num = iter_num;
        this.topic_num = topic_num;
        this.data_path = data_path;
        this.outputPath = (new File(data_path)).getParentFile().getAbsolutePath() + "/LDA_one_word_" + alpha + "_" + beta + "_" + topic_num + "/" + threadId + "/";
        (new File(this.outputPath)).mkdirs();
        print_parameter();
    }

    public static void build_LDA_Model(String data_path, int topic_num, int iter_num, double alpha, double beta, int threadNum) {

        ExecutorService service = Executors.newCachedThreadPool();
        for (int threadId = 0; threadId < threadNum; threadId++) {
            LDA_one_word model = new LDA_one_word(data_path, topic_num, iter_num, alpha, beta, threadId);
            service.submit(model);
        }
        service.shutdown();

    }

    private void print_parameter() {
        System.out.println("path:" + this.data_path
                + "\talpha:" + this.alpha
                + "\tbeta:" + this.beta
                + "\titer_num:" + this.iter_num
                + "\ttopic_num:" + this.topic_num
        );
    }

    private void load_data() {
        try {
            BufferedReader reader = IOUtil.getReader(this.data_path);
            String line;
            ArrayList<int[]> tmpCorpus = new ArrayList<>();
            while ((line = reader.readLine()) != null) {
                String[] words = line.split("\\s+");
                int[] oneDoc = new int[words.length];
                for (int wordIndex = 0; wordIndex < words.length; wordIndex++) {
                    if (!this.wordMap.containsKey(words[wordIndex])) {
                        this.wordMap.put(words[wordIndex], this.wordMap.size());
                        this.id2Word.put(this.wordMap.get(words[wordIndex]), words[wordIndex]);
                        this.wordId2TopicId.put(this.wordMap.get(words[wordIndex]), (int) (Math.random() * this.topic_num));
                        this.wordNum.put(this.wordMap.get(words[wordIndex]), 0);
                    }
                    oneDoc[wordIndex] = this.wordMap.get(words[wordIndex]);
                    this.wordNum.put(this.wordMap.get(words[wordIndex]), this.wordNum.get(this.wordMap.get(words[wordIndex])) + 1);
                }
                tmpCorpus.add(oneDoc);
            }
            reader.close();

            this.wordId_of_corpus = new int[tmpCorpus.size()][];
            for (int docIndex = 0; docIndex < this.wordId_of_corpus.length; docIndex++) {
                this.wordId_of_corpus[docIndex] = tmpCorpus.get(docIndex);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void init_model() {
        this.word_num_in_topic_word = new int[this.topic_num][this.wordMap.size()];
        this.word_num_in_topic = new int[this.topic_num];
        this.word_num_in_doc_topic = new int[this.wordId_of_corpus.length][this.topic_num];

        for (int docIndex = 0; docIndex < this.wordId_of_corpus.length; docIndex++) {
            for (int wordIndex = 0; wordIndex < this.wordId_of_corpus[docIndex].length; wordIndex++) {
                int wordId = this.wordId_of_corpus[docIndex][wordIndex];
                int topicId = this.wordId2TopicId.get(wordId);
                this.word_num_in_topic_word[topicId][wordId] += 1;
                this.word_num_in_topic[topicId] += 1;
                this.word_num_in_doc_topic[docIndex][topicId] += 1;

                ArrayList<Integer> wordDoc = this.wordInDocIndex.get(wordId);
                if (wordDoc == null) {
                    wordDoc = new ArrayList<>();
                    this.wordInDocIndex.put(wordId, wordDoc);
                }
                wordDoc.add(docIndex);
            }
        }
    }

    private void save_twords(int topWordNum) throws IOException {
        BufferedWriter writer = IOUtil.getWriter(this.outputPath + "model-final.twords");

        for (int topic_index = 0; topic_index < this.word_num_in_topic_word.length; topic_index++) {
            HashMap<Integer, Double> oneLine = new HashMap<>();
            for (int word_id = 0; word_id < this.word_num_in_topic_word[topic_index].length; word_id++) {
                oneLine.put(word_id, ((double) this.word_num_in_topic_word[topic_index][word_id]) / this.word_num_in_topic[topic_index]);
            }
            List<Map.Entry<Integer, Double>> maplist = new ArrayList<>(oneLine.entrySet());

            Collections.sort(maplist, new Comparator<Map.Entry<Integer, Double>>() {
                public int compare(Map.Entry<Integer, Double> o1, Map.Entry<Integer, Double> o2) {
                    return (o2.getValue().compareTo(o1.getValue()));
                }
            });

            writer.write("Topic:" + topic_index + "\n");
            int count = 0;
            for (Map.Entry<Integer, Double> o1 : maplist) {
                writer.write("\t" + this.id2Word.get(o1.getKey()) + "\t:" + o1.getValue() + "\n");
                count++;
                if (count > topWordNum) {
                    break;
                }
            }
            writer.write("\n");

        }


        writer.close();
    }

    private void save_wordMap() throws IOException {
        BufferedWriter writer = IOUtil.getWriter(this.outputPath + "model-final.wordmap");
        for (String key : this.wordMap.keySet()) {
            writer.write(key + " " + this.wordMap.get(key));
            writer.write("\n");
        }
        writer.close();
    }

    private void save_theta() throws IOException {
        BufferedWriter writer = IOUtil.getWriter(this.outputPath + "model-final.theta");
        int doc_index = 0;
        for (int[] line : this.word_num_in_doc_topic) {
            for (int topic : line) {
                writer.write((this.alpha + topic) / (this.wordId_of_corpus[doc_index].length + this.alpha * this.topic_num) + " ");
            }
            writer.write("\n");
            doc_index++;
        }
        writer.close();
    }

    private void save_tassign() throws IOException {
        BufferedWriter writer = IOUtil.getWriter(this.outputPath + "model-final.tassign");
        for (int doc_index = 0; doc_index < this.wordId_of_corpus.length; doc_index++) {
            for (int word_index = 0; word_index < this.wordId_of_corpus[doc_index].length; word_index++) {
                writer.write(this.wordId_of_corpus[doc_index][word_index] + ":"
                        + this.wordId2TopicId.get(this.wordId_of_corpus[doc_index][word_index]) + " ");
            }
            writer.write("\n");
        }
        writer.close();
    }

    private void save_phi() throws IOException {
        BufferedWriter writer = IOUtil.getWriter(this.outputPath + "model-final.phi");
        int topic_index = 0;
        for (int[] line : this.word_num_in_topic_word) {
            for (int words : line) {
                writer.write((this.beta + words) / (this.word_num_in_topic[topic_index] + this.beta * this.wordMap.size()) + " ");
            }
            writer.write("\n");
            topic_index++;
        }
        writer.close();

    }

    private int sample(int wordId) {
        int oldTopicId = this.wordId2TopicId.get(wordId);
        int newTopicId = -1;
/////////////////////////////////////////////////////////////////////////////////////////////
        this.word_num_in_topic_word[oldTopicId][wordId] -= this.wordNum.get(wordId);
        this.word_num_in_topic[oldTopicId] -= this.wordNum.get(wordId);
        for (int docIndex : this.wordInDocIndex.get(wordId)) {
            this.word_num_in_doc_topic[docIndex][oldTopicId] -= 1;
        }
/////////////////////////////////////////////////////////////////////////////////////////////


        double[] p = new double[this.topic_num];


        for (int docIndex : this.wordInDocIndex.get(wordId)) {
            double[] inner_p = new double[this.topic_num];
            double sum = 0;
            for (int k = 0; k < this.topic_num; k++) {

                inner_p[k] = (word_num_in_doc_topic[docIndex][k] + alpha)
                        * (this.wordNum.get(wordId) + beta)
                        / (word_num_in_topic[k] + this.wordNum.get(wordId) + word_num_in_topic_word[0].length * beta);
                sum += inner_p[k];

            }
            for (int k = 0; k < this.topic_num; k++) {
                p[k] += inner_p[k] / sum;
            }
        }
        //////////////////////////////////////////////////////////////////////////////////

        for (int k = 1; k < this.topic_num; k++) {
            p[k] += p[k - 1];
        }
        double u = Math.random() * p[this.topic_num - 1];
        for (int k = 0; k < this.topic_num; k++) {
            if (u < p[k]) {
                newTopicId = k;
                break;
            }

        }

///////////////////////////////////////////////////////////////////////////////////////////
        this.word_num_in_topic_word[newTopicId][wordId] += this.wordNum.get(wordId);
        this.word_num_in_topic[newTopicId] += this.wordNum.get(wordId);
        for (int docIndex : this.wordInDocIndex.get(wordId)) {
            this.word_num_in_doc_topic[docIndex][newTopicId] += 1;
        }
////////////////////////////////////////////////////////////////////////////////////////////
        assert newTopicId > -1;
        return newTopicId;

    }

    private void build_model() {

        for (int iter = 0; iter < this.iter_num; iter++) {
            long startTime = System.currentTimeMillis();
            for (int wordId : this.wordId2TopicId.keySet()) {
                this.wordId2TopicId.put(wordId,
                        sample(wordId));
            }
            System.out.println("finished iter :" + iter + "\tcost time:" + ((double) System.currentTimeMillis() - startTime) / 1000);
        }

    }

    private void save_result() {
        try {
            this.save_twords(20);
            this.save_theta();
            this.save_tassign();
            this.save_wordMap();
            this.save_phi();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        this.load_data();
        this.init_model();
        this.build_model();
        this.save_result();
    }


}
